package imoney.server;


import static imoney.server.util.CookieBuilderServer.*;
import imoney.server.util.DataStoreManager;
import imoney.shared.exception.NoSuchPersonException;
import imoney.shared.exception.PositionOutOfDateException;
import imoney.shared.model.Person;

import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

import static imoney.shared.Statics.*;

/**
 * A servlet registering users in the context for future reference, and forwarding
 * the request to the page initially requested. This servlet can be called from any
 * other servlet when they find a non-instanciated user (the user is not 'inflated'
 * and stored into the temporary list of connected Persons). 
 *
 * @author Arnaud PELOQUIN
 *
 */
public class UserServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;
	Logger log = Logger.getLogger(UserServlet.class.getName());

	@Override
	public void destroy(){
		/*
		 * can be used to save or close connexions that are not
		 * cleaned by the garbage collector, or persist data that
		 * can be used during the next init().
		 */
	}

	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException{
		// verify user is logged to his google account
		UserService userService = UserServiceFactory.getUserService();
		User user = userService.getCurrentUser();
		if (user == null) {
			System.out.println(UserServlet.class.getName() + " : redirection to this servlet after google identification.");
			resp.sendRedirect(userService.createLoginURL(LOGGIN_URL));
			return;
		}
		// try to get information from cookie
		Cookie[] cookiesBox = req.getCookies();
		Cookie indexCookie = null;
		for (Cookie cook : cookiesBox) {
			if (cook.getName().equals(Cookie_INDEX_OF_PERSON)) {
				indexCookie = cook;
				break;
			}
		}
		// if cookie found and corresponding to the user, use it
		String userID = user.getUserId();
		if (indexCookie != null && userID.equals(readUserId(indexCookie))) {
			int position = readPosition(indexCookie);
			String pseudo = readPseudo(indexCookie);
			try {
				// getPerson() only used as a verifier here
				getPerson(position, userID, getServletContext());
				redirectToApp(req, resp);
				return;
			} catch (PositionOutOfDateException e) {
				// if the person is in context, just update cookie
				Cookie cookie = buildIndexCookie(userID, e.getRealPosition(), pseudo);
				resp.addCookie(cookie);
				redirectToApp(req, resp);
				return;
			}
			catch (NoSuchPersonException e) {
				// logging is performed out of the 'if' block
			}
		}
		// no valid cookie data, or user not in context. login from database
		if(logPersonIn(user, resp)) {
			redirectToApp(req, resp);
		} else {
			// user is not in database, go to account creation
			redirectToAccount(req, resp);
		}
	}

	/**
	 * Looks for the specified person at the specified position in list.
	 * If not corresponding, the list is then searched.
	 * <br><br>
	 * Can be used by the callbacks to retrieve the corresponding person.
	 * 
	 * @param startPosition : the position to hit.
	 * @param googleID : the ID of the person.
	 * @param context : where to get the list of logged-in persons. as the method is static,
	 * the servletContext cannot be retrieved in-method, so it has to be served as a parameter.
	 * @return person : the instance of the person. should never be null,
	 * because an exception is thrown in case of a problem
	 * 
	 * @throws NoSuchPersonException Exception indicating that the person is not at the
	 * indicated position, so the cookie is out of date. Redirect to Statics.LOGGIN_URL.
	 * @throws IndexOutOfBoundsException Exception indicating that the list have been shortened,
	 * so the cookie is out of date. Redirect to Statics.LOGGIN_URL.
	 * @throws PositionOutOfDateException if the person is in the context but given
	 * position is not right.
	 */
	public static Person getPerson(int position, String googleID, ServletContext context)
			throws NoSuchPersonException, PositionOutOfDateException {
		@SuppressWarnings("unchecked")
		ArrayList<Person> personList = (ArrayList<Person>) context.getAttribute(Context_LIST_PERSONS);
		// first check that the position is supported by the list
		if (position >= personList.size()) {
			int realPosition = searchContext(googleID, context);
			if (realPosition >= 0) {
				throw new PositionOutOfDateException(realPosition);
			} else {
				throw new NoSuchPersonException();
			}
		}
		// then check that the googleID is matching
		String retrievedGoogleID = personList.get(position).getUserID();
		if (!retrievedGoogleID.equals(googleID)) {
			int realPosition = searchContext(googleID, context);
			if (realPosition >= 0) {
				throw new PositionOutOfDateException(realPosition);
			} else {
				throw new NoSuchPersonException();	
			}
		}
		return personList.get(position);
	}

	public static boolean updatePersonInContext(String googleID, ServletContext context, int position) {
		@SuppressWarnings("unchecked")
		ArrayList<Person> personList = (ArrayList<Person>) context.getAttribute(Context_LIST_PERSONS);
		boolean result = false;
		if (position >= 0) {
			Person update = DataStoreManager.getPerson(googleID);
			personList.set(position, update);
			result = true;
		}
		return result;
	}

	/**
	 * A method that fetches the person in the database, instanciates it,
	 * and adds it to the context (which should contain a list of Person).
	 * <br><br>
	 * A Cookie is added to the response to enable authentified navigation throught the app.
	 * <br><br>
	 * Other servlet can acces this method via request&response forwarding to this servlet.
	 * @return true if the logging was successful, and false if the person was not found in
	 * the database.
	 * @param user : the instance of User returned by google UserService
	 * @param resp : the HttpServletResponse where to add the cookie.
	 */
	private boolean logPersonIn(User user, HttpServletResponse resp){
		ServletContext context = getServletContext();
		// try to retrieve the person in the database
		try{
			int indexOfUser = genericLoginPerson(user.getUserId(),context);
			@SuppressWarnings("unchecked")
			ArrayList<Person> personList = (ArrayList<Person>) context.getAttribute(Context_LIST_PERSONS);
			Person person = personList.get(indexOfUser);
			Cookie cookie = buildIndexCookie(user.getUserId(), indexOfUser, person.getPseudo());
			resp.addCookie(cookie);
			return true;
		}
		// if not, go to account creation
		catch(NoSuchPersonException ex){
			return false;
		}
	}

	/**
	 * Finds in the database, instanciate and adds the person to the context. Returns the index where the person was added.
	 * If the googleID served in param is not found, no further action is taken (no row added to database),
	 * and an exception is thrown.
	 * @param googleID
	 * @param context
	 * @return : The index of the Person in the context list. It should always be consistent (no -1 should appear).
	 * @throws NoSuchPersonException : if the person wasn't found in the database (search by googleID)
	 */
	public static int genericLoginPerson(String googleID, ServletContext context)
			throws NoSuchPersonException {
		Person pers = DataStoreManager.getPerson(googleID);
		if(pers == null){
			throw new NoSuchPersonException("Unable to find this Person in the database : "
					+ googleID + " (google userID)");
		}
		@SuppressWarnings("unchecked")
		ArrayList<Person> personList = (ArrayList<Person>) context.getAttribute(Context_LIST_PERSONS);
		int freePosition = -1;
		for (int i=0; i<personList.size(); i++) {
			Person tempPers = personList.get(i);
			if (tempPers == null) {
				freePosition = i;
				// all the list has to be parsed to ensure the person is not already in
				continue;
			}
			if(tempPers.equals(pers)){
				// the person is already logged in, just in case perform an update.
				personList.set(i, pers);
				return i; 
			}
		}
		// finally, adding the person to the list
		if (freePosition >= 0) {
			personList.set(freePosition, pers);
		} else {
			freePosition = personList.size(); // as the list is full, point after the end of the list.
			personList.add(pers); // append the Person at the end of the list.
		}
		return freePosition;
	}

	/**
	 * This method reads the request to find the page wanted before authentication
	 * was triggered, and redirects to it if found. If no page was wanted before
	 * the authentication, redirection to application's Home.
	 * @param req
	 * @param resp
	 */
	private void redirectToApp(HttpServletRequest req, HttpServletResponse resp) {
		// TODO find a way to use the UrlUtils class to centralize redirection methods.
		String Url = req.getParameter(SRC_URL_PARAM);
		if(Url == null) {
			Url = HOME_URL;
		}
		try {	
			resp.sendRedirect(Url);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	private void redirectToAccount(HttpServletRequest req,
			HttpServletResponse resp) {
		String url = ACCOUNT_URL;
		String source = req.getParameter(SRC_URL_PARAM);
		if (source != null) {
			url += "?" + SRC_URL_PARAM + "=" + source ;				
		}
		try {
			resp.sendRedirect(url);
			log.info("redirecting to:"+url);
		} catch (IOException ioex) {
			ioex.printStackTrace();
		}
	}

	/**
	 * Perform a throrough search of the person in the context's person list and return
	 * the first position matching with the googleID. No "doubles" check.
	 * @param googleID : the userID returned by google services.
	 * @param context : the context where to find the list of logged in persons.
	 * @return position : the position of the person if found, or -1 otherwise.
	 */
	private static int searchContext(String googleID, ServletContext context) {
		@SuppressWarnings("unchecked")
		ArrayList<Person> personList = (ArrayList<Person>) context.getAttribute(Context_LIST_PERSONS);
		int position = -1;
		for (int i=0; i<personList.size(); i++) {
			Person pers = personList.get(i);
			if( pers != null && pers.getUserID().equals(googleID) ){
				position = i;
				break;
			}
		}
		return position;
	}

	/*
	 *  TODO : migrate the logout method in this class, in order to centralise the authentication
	 *  functions and minimize error risk.
	 */


	/*
	 * String getMethod() : retourne le type de requête
	 * String getServeurName() : retourne le nom du serveur
	 * String getParameter(String name) : retourne la valeur d'un paramètre
	 * String[] getParameterNames() : retourne le nom des les paramètres
	 * String getRemoteHost() : retourne l'IP du client
	 * String getServerPort() : retourne le port sur lequel le serveur écoute
	 * String getQueryString() : retourne la chaîne d’interrogation
	 */
}
